package net.es.weathermap.topo.ps;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;

import org.apache.commons.httpclient.HttpException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.filter.ElementFilter;

import org.apache.log4j.*;

import edu.internet2.perfsonar.PSException;

import net.es.weathermap.beans.AggrConnection;
import net.es.weathermap.beans.Connection;
import net.es.weathermap.beans.Domain;
import net.es.weathermap.beans.Hub;
import net.es.weathermap.beans.IfceConnection;
import net.es.weathermap.beans.Node;
import net.es.weathermap.topo.TopoUtils;
import net.es.weathermap.topo.TopologyProvider;
import net.es.weathermap.topo.URNParser;
import net.es.weathermap.topo.URNParserResult;
import net.es.weathermap.topo.sites.SiteInfo;
import net.es.weathermap.topo.network.NetworkInfo;
import net.es.weathermap.topo.network.PeerInfo;

public class PerfsonarTP implements TopologyProvider {
    private static Domain domain;
    private String localDomainId;
    private String delimiter;
    private static Logger LOG = Logger.getLogger(PerfsonarTP.class);

    public PerfsonarTP() {
        // get local topology id
        localDomainId = NetworkInfo.getInstance().getNetwork();
        // get delimiter in all router names
        delimiter = NetworkInfo.getInstance().getDelimiter();
    }

    public Domain getTopology() {
        if (domain != null) {
            return domain;
        }
        PSTopoPuller puller = new PSTopoPuller();
        Element domXML = null;
        try {
            domXML = puller.pullTopology();
        } catch (HttpException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (PSException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        domain = this.parseTopology(domXML);
        this.makeAggrLinks(domain);
        return domain;
    }

    @SuppressWarnings("unchecked")
    private Domain parseTopology(Element domXML) {

        Domain domain = new Domain();
        domain.setTopoId(localDomainId);
        Long secs = System.currentTimeMillis() / 1000;
        Integer timestamp = secs.intValue();
        domain.setTimestamp(timestamp);
        domXML.detach();
        Document doc = new Document(domXML);
        Namespace nmtb = Namespace.getNamespace("nmtb", "http://ogf.org/schema/network/topology/base/20070828/");
        Namespace nmtl2 = Namespace.getNamespace("nmtl2", "http://ogf.org/schema/network/topology/l2/20070828/");
//        Namespace nmtl3 = Namespace.getNamespace("nmtl3", "http://ogf.org/schema/network/topology/l3/20070828/");
//        Namespace nmtl4 = Namespace.getNamespace("nmtl4", "http://ogf.org/schema/network/topology/l4/20070828/");

        ArrayList<Hub> hubs = new ArrayList<Hub>();
        HashMap<String, Node> nodes = new HashMap<String, Node>();

        ElementFilter nodeFilter = new ElementFilter("node", nmtb);
        ElementFilter portFilter = new ElementFilter("port", nmtl2);
        ElementFilter linkFilter = new ElementFilter("link", nmtl2);
        ElementFilter relFilter  = new ElementFilter("relation", nmtb);

        Iterator<Element> nodeXMLIt =  (Iterator<Element>) doc.getDescendants(nodeFilter);
        while (nodeXMLIt.hasNext()) {
            Element nodeXML = nodeXMLIt.next();
            String name = nodeXML.getChildText("name", nmtb);
            String latitude = nodeXML.getChildText("latitude", nmtb);
            String longitude = nodeXML.getChildText("longitude", nmtb);
            Node router = new Node();
            router.setType("router");
            router.setTopoId(name);
            router.setLatitude(latitude);
            router.setLongitude(longitude);
            ArrayList<Integer> lods = new ArrayList<Integer>();
            lods.add(1);
            nodes.put(name, router);
        }
        // get site info map
        HashMap<String,HashMap<String, String>> siteInfo =
            SiteInfo.getInstance().getSites();
        // get peer interface info map
        HashMap<String,HashMap<String, Object>> xfaceInfo =
            PeerInfo.getInstance().getXfaces();
        // get peer info map
        HashMap<String,HashMap<String, String>> peerInfo =
            PeerInfo.getInstance().getPeers();
        if (peerInfo == null) {
            LOG.info("peerInfo is null");
        }
        // reset iterator
        nodeXMLIt =  (Iterator<Element>) doc.getDescendants(nodeFilter);
        while (nodeXMLIt.hasNext()) {
            Element nodeXML = nodeXMLIt.next();
            Iterator<Element> portXMLIt =  (Iterator<Element>) nodeXML.getDescendants(portFilter);
            String name = nodeXML.getChildText("name", nmtb);
            //LOG.info("router: " + name);
            Node router = nodes.get(name);
            Node site = null;
            ArrayList<IfceConnection> ipIfceConnections = new ArrayList<IfceConnection>();
            ArrayList<IfceConnection> sdnIfceConnections = new ArrayList<IfceConnection>();
            ArrayList<IfceConnection> peerIfceConnections = new ArrayList<IfceConnection>();
            router.setIfceConnections(Connection.IP, ipIfceConnections);
            router.setIfceConnections(Connection.SDN, sdnIfceConnections);
            router.setIfceConnections(Connection.PEER, peerIfceConnections);

            while (portXMLIt.hasNext()) {
                Element portXML = portXMLIt.next();
                Integer capacity = Integer.valueOf(portXML.getChildText("capacity", nmtl2));
                String portFQTI = portXML.getChildText("ifName", nmtl2);
                String description = portXML.getChildText("ifDescription", nmtl2);
                String type = Connection.IP;
                if (description.contains(":sdn")) {
                    type = Connection.SDN;
                }
                String[] parts = description.split("\\:");
                URNParserResult portRes =
                    URNParser.parseTopoIdent(portFQTI, localDomainId);
                String ifName = portRes.getPortId();
                // TODO:  take out after SC
                if ((name.equals("pnwg-cr1") && ifName.equals("xe-6/1/0")) ||
                    (name.equals("pnwg-sdn1") && ifName.equals("xe-7/3/0")) ||
                    (name.equals("sunn-sdn2") && ifName.equals("xe-7/3/0"))) {
                    this.handleSite(nodes, siteInfo, name, "SC09", ifName,
                            ipIfceConnections, sdnIfceConnections,
                            router, type, capacity, description);
                }
                if (description.contains(":site-ex")) {
                    parts = parts[0].split("\\->");
                    //LOG.info("Node: " + parts[0]);
                    parts = parts[1].split("\\-");
                    parts = parts[0].split("\\(");
                    //LOG.info("Site: " + parts[0]);
                    // manually remove for now
                    if (parts[0].equals("pnnl") && name.equals("osti-rt1")) {
                        continue;
                    }
                    this.handleSite(nodes, siteInfo, name, parts[0], ifName,
                            ipIfceConnections, sdnIfceConnections,
                            router, type, capacity, description);
                } else {
                    // handle peers if present
                    this.handlePeers(nodes, xfaceInfo, peerInfo, name, ifName,
                                peerIfceConnections,
                                router, capacity, description);
                }
                /*
                if (description.contains(":priv-ex")) {
                    LOG.info("Peer: "+description);
                }
                
                if (description.contains(":com-intercloud")) {
                    LOG.info("COM: "+description);
                }
                if (description.contains(":edu-intercloud")) {
                    LOG.info("EDU: "+description);
                }
                if (description.contains(":ren-intercloud")) {
                    LOG.info("REN: "+description);
                }
                if (description.contains(":int-intercloud")) {
                    LOG.info("INT: "+description);
                }
                if (description.contains(":perf-test")) {
                    LOG.info("PT: "+description);
                }
                */
                Iterator<Element> linkXMLIt =  (Iterator<Element>) portXML.getDescendants(linkFilter);
                while (linkXMLIt.hasNext()) {
                    Element linkXML = linkXMLIt.next();
                    Iterator<Element> relXMLIt =  (Iterator<Element>) linkXML.getDescendants(relFilter);
                    while (relXMLIt.hasNext()) {
                        String linkFQTI = linkXML.getAttributeValue("id");

                        URNParserResult res = URNParser.parseTopoIdent(linkFQTI, localDomainId);
                        String subIfName = res.getLinkId();
                        //LOG.info(name + ":" + subIfName);

                        Element relXML = relXMLIt.next();
                        if (relXML.getAttribute("type").getValue().equals("sibling")) {
                            String remLinkFQTI = relXML.getChildText("idRef", nmtb);
                            res = URNParser.parseTopoIdent(remLinkFQTI, localDomainId);
                            String remRouterName = res.getNodeId();
                            String remLinkId = res.getLinkId();
                            if (nodes.get(remRouterName) != null) {
                                IfceConnection ic = new IfceConnection();
                                ic.setIfceId(name+":"+subIfName);
                                ic.setRemoteIfceId(remRouterName+":"+remLinkId);
                                ic.setType(type);
                                ic.setSrc(router);
                                ic.setCapacity(capacity);
                                ic.setDescription(description);
                                ic.setDst(nodes.get(remRouterName));
                                if (type.equals(Connection.SDN)) {
                                    sdnIfceConnections.add(ic);
                                } else {
                                    ipIfceConnections.add(ic);
                                }
                                if (ic.getDst().getIfceConnections(type) != null ) {
                                    for (IfceConnection tmpIc : ic.getDst().getIfceConnections(type)) {
                                        if (tmpIc.getIfceId().equals(ic.getRemoteIfceId())) {
                                            tmpIc.setReverse(ic);
                                            ic.setReverse(tmpIc);
                                        }
                                    }
                                }
                            }
                            else {
                                LOG.info("remote router: " + remRouterName);
                            }
                        }
                    }
                }
            }
        }
        this.splitNodes(hubs, nodes);
        domain.setHubs(hubs);
        return domain;
    }

    private void splitNodes(List<Hub> hubs, Map<String, Node> nodes) {
        Iterator<String> nodeIt = nodes.keySet().iterator();
        // split nodes into hubs
        while (nodeIt.hasNext()) {
            String name = nodeIt.next();
            Node node = nodes.get(name);
            if (hubs.isEmpty()) {
                Hub hub = this.makeHub(node);
                hubs.add(hub);
                String[] parts = name.split(this.delimiter);
                // may be overwritten when getting aggregate links, which gets
                // the real hubs
                hub.setTopoId(parts[0].toUpperCase());
            } else {
                boolean gotHub = false;
                Hub tmpHub = null;
                for (Hub hub : hubs) {
                    for (Node otherNode : hub.getNodes()) {
                        Double distance = TopoUtils.distance(node.getLatitude(), node.getLongitude(),
                                    otherNode.getLatitude(), otherNode.getLongitude());
                        if (!gotHub && distance < 100.0) {
                            gotHub = true;
                            tmpHub = hub;
                        }
                    }
                }
                if (!gotHub) {
                    //LOG.info("making " + name + " into a hub");
                    Hub hub = this.makeHub(node);
                    hubs.add(hub);
                    String[] parts = name.split(this.delimiter);
                    hub.setTopoId(parts[0].toUpperCase());
                } else {
                    /*
                    LOG.info("got node " + node.getTopoId() + " for " +
                            tmpHub.getTopoId());
                    */
                    tmpHub.getNodes().add(node);
                    node.setParent(tmpHub);
                }
            }
        }
    }

    private void
        handleSite(HashMap<String, Node> nodes,
             HashMap<String, HashMap<String, String>> siteInfo,
             String routerName, String siteName, String ifName,
             List<IfceConnection> ipConnections,
             List<IfceConnection> sdnConnections,
             Node router, String type, Integer capacity, String description) {

        String routerType = "site";
        // kludge for SC 09
        if (siteName.equals("SC09")) {
            routerType = "router";
        }
        Node site = this.addNode(siteInfo, nodes, siteName, routerType);
        if (site != null) {
            IfceConnection ic =
                this.addNodeRouterIc(routerName, ifName, site.getTopoId(), type,
                                     router, capacity, description, site);
            if (type.equals(Connection.SDN)) {
                sdnConnections.add(ic);
            } else {
                ipConnections.add(ic);
            }
            if (ic.getDst().getIfceConnections(type) != null ) {
                boolean foundRemoteIc = false;
                for (IfceConnection tmpIc: ic.getDst().getIfceConnections(type)) {
                    if (tmpIc.getIfceId().equals(ic.getRemoteIfceId())) {
                        foundRemoteIc = true;
                    }
                }
                if (!foundRemoteIc) {
                    List<IfceConnection> remoteIcs =
                        ic.getDst().getIfceConnections(type);
                    IfceConnection remoteIc =
                        this.addNodeRouterIc(routerName, ifName, site.getTopoId(),
                                     type, site, capacity, description, router);
                    remoteIcs.add(remoteIc);
                    remoteIc.setReverse(ic);
                    ic.setReverse(remoteIc);
                }
            }
        }       
    }

    private void
        handlePeers(HashMap<String, Node> nodes,
             HashMap<String, HashMap<String, Object>> xfaceInfo,
             HashMap<String, HashMap<String, String>> peerInfo,
             String routerName, String ifName,
             List<IfceConnection> peerConnections,
             Node router, Integer capacity, String description) {

        String type = Connection.PEER;
        String key = routerName + ":" + ifName;
        if (!xfaceInfo.containsKey(key)) {
            return;
        }
        HashMap<String, ArrayList<String>> subXfaces =
            (HashMap<String, ArrayList<String>>) xfaceInfo.get(key).get("subInterfaces");
        if (subXfaces == null) {
            return;
        }
        for (String subXface: subXfaces.keySet()) {
            List<String> peerNames = (ArrayList<String>) subXfaces.get(subXface);
            for (String peerName: peerNames) {
                Node peer = this.addNode(peerInfo, nodes, peerName, "peer");
                if (peer == null) {
                    continue;
                }
                String subInterfaceName = ifName;
                if (!subXface.equals("*")) {
                    subInterfaceName += "." + subXface;
                }
                IfceConnection ic =
                    this.addNodeRouterIc(routerName, subInterfaceName,
                                         peer.getTopoId(), type,
                                         router, capacity, description, peer);
                peerConnections.add(ic);
                if (ic.getDst().getIfceConnections(type) != null ) {
                    boolean foundRemoteIc = false;
                    for (IfceConnection tmpIc: ic.getDst().getIfceConnections(type)) {
                        if (tmpIc.getIfceId().equals(ic.getRemoteIfceId())) {
                            foundRemoteIc = true;
                        }
                    }
                    if (!foundRemoteIc) {
                        List<IfceConnection> remoteIcs =
                            ic.getDst().getIfceConnections(type);
                        IfceConnection remoteIc =
                            this.addNodeRouterIc(routerName, subInterfaceName,
                                    peer.getTopoId(), type, peer, capacity,
                                    description, router);
                        remoteIcs.add(remoteIc);
                        remoteIc.setReverse(ic);
                        ic.setReverse(remoteIc);
                    }
                }
            }
        }       
    }

    private Node addNode(HashMap<String, HashMap<String, String>> nodeInfo,
                           HashMap<String, Node> nodes, String nodeName,
                           String nodeType) {

        if (!nodeInfo.containsKey(nodeName)) {
            return null;
        }
        Node node = new Node();
        Map<String, String> nodeData = nodeInfo.get(nodeName);
        if (nodeData.containsKey("nodisplay")) {
            return null;
        }
        // TODO:  null checks
        // if site has an alias in the interface descriptions
        if (nodeData.containsKey("alias")) {
            nodeName = nodeData.get("alias");
            nodeData = nodeInfo.get(nodeName);
        }
        // depends on current convention of a router name having two parts,
        // also depends on no site name being equivalent to peer name
        if (!nodes.containsKey(nodeName)) {
            node.setTopoId(nodeName);
            node.setFullName(nodeData.get("name"));
            node.setLatitude(nodeData.get("latitude"));
            node.setLongitude(nodeData.get("longitude"));
            if (nodeData.get("lod") != null) {
                node.setLod(nodeData.get("lod"));
            } else {
                node.setLod("0");
            }
            ArrayList<IfceConnection> ipIfceConnections = new ArrayList<IfceConnection>();
            ArrayList<IfceConnection> sdnIfceConnections = new ArrayList<IfceConnection>();
            ArrayList<IfceConnection> peerIfceConnections = new ArrayList<IfceConnection>();
            node.setIfceConnections(Connection.IP, ipIfceConnections);
            node.setIfceConnections(Connection.SDN, sdnIfceConnections);
            node.setIfceConnections(Connection.PEER, peerIfceConnections);
            node.setType(nodeType);
            nodes.put(nodeName, node);
        } else {
            node = nodes.get(nodeName);
        }
        return node;
    }

    private IfceConnection addNodeRouterIc(String srcName, String ifName,
            String dstName, String type, Node src,
            Integer capacity, String description, Node dst) {

        IfceConnection ic = new IfceConnection();
        ic.setIfceId(srcName+":"+ifName);
        ic.setRemoteIfceId(dstName + ":" + ifName);
        ic.setType(type);
        ic.setSrc(src);
        ic.setCapacity(capacity);
        ic.setDescription(description);
        ic.setDst(dst);
        return ic;
    }

    private Node addPeer(HashMap<String, HashMap<String, String>> peerInfo,
                           HashMap<String, Node> nodes, String peerName) {

        Node peer = null;
        return peer;
    }

    private void makeAggrLinks(Domain domain) {
        for (String type : new String[] {Connection.IP, Connection.SDN, Connection.PEER } ) {
            for (Hub hub : domain.getHubs()) {
                if (hub.getAggrConnections(type) == null) {
                    hub.setAggrConnections(type, new ArrayList<AggrConnection>());
                }
                for (Node node : hub.getNodes()) {
                    // make aggregated connections between nodes
                    if (node.getAggrConnections(type) == null) {
                        node.setAggrConnections(type, new ArrayList<AggrConnection>());
                    }
                    for (IfceConnection conn : node.getIfceConnections(type)) {
                        if (!hub.getNodes().contains(conn.getDst())) {
                            for (Hub otherHub : domain.getHubs()) {
                                if (otherHub.getNodes().contains(conn.getDst())) {
                                    AggrConnection toOtherHub = null;
                                    for (AggrConnection aggrConn : hub.getAggrConnections(type)) {
                                        if (aggrConn.getDst().equals(otherHub)) {
                                            toOtherHub = aggrConn;
                                        }
                                    }
                                    if (toOtherHub == null) {
                                        toOtherHub = new AggrConnection();
    
                                        toOtherHub.setSrc(hub);
                                        toOtherHub.setDst(otherHub);
                                        toOtherHub.setCapacity(conn.getCapacity());
                                        ArrayList<String> ifceIds = new ArrayList<String>();
                                        ifceIds.add(conn.getIfceId());
                                        toOtherHub.setIfceIds(ifceIds);
                                        toOtherHub.setType(type);
                                        // if get to here, this node is the
                                        // real hub
                                        String[] parts =
                                            node.getTopoId().split(this.delimiter);
                                        hub.setTopoId(parts[0].toUpperCase());
                                        hub.setLatitude(node.getLatitude());
                                        hub.setLongitude(node.getLongitude());
                                        hub.getAggrConnections(type).add(toOtherHub);
                                    } else {
                                        toOtherHub.setCapacity(toOtherHub.getCapacity() + conn.getCapacity());
                                        toOtherHub.getIfceIds().add(conn.getIfceId());
                                    }
                                    if (toOtherHub.getDst().getAggrConnections(type) != null) {
                                        for (AggrConnection tmpAc : toOtherHub.getDst().getAggrConnections(type)) {
                                            if (tmpAc.getDst().equals(toOtherHub.getSrc())) {
                                                tmpAc.setReverse(toOtherHub);
                                                toOtherHub.setReverse(tmpAc);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        boolean found = false;
                        for (AggrConnection aggrConn : node.getAggrConnections(type)) {
                            if (aggrConn.getDst().equals(conn.getDst())) {
                                found = true;
                                aggrConn.setCapacity(aggrConn.getCapacity()+conn.getCapacity());
                                aggrConn.getIfceIds().add(conn.getIfceId());
                            }
                        }
                        if (!found) {
                            AggrConnection ac = new AggrConnection();
                            ac.setSrc(conn.getSrc());
                            ac.setDst(conn.getDst());
                            ac.setDescription("");
                            ac.setType(type);
                            ac.setIfceIds(new ArrayList<String>());
                            ac.getIfceIds().add(conn.getIfceId());
                            ac.setCapacity(conn.getCapacity());
                            node.getAggrConnections(type).add(ac);
                            if (ac.getDst().getAggrConnections(type) != null) {
                                for (AggrConnection tmpAc : ac.getDst().getAggrConnections(type)) {
                                    if (tmpAc.getDst().equals(ac.getSrc())) {
                                        tmpAc.setReverse(ac);
                                        ac.setReverse(tmpAc);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    private Hub makeHub(Node node) {
        // TODO: find lat / lon thru http://www.geomidpoint.com/calculation.html
        Hub hub = new Hub();
        hub.setTopoId(node.getTopoId());
        hub.setLatitude(node.getLatitude());
        hub.setLongitude(node.getLongitude());
        hub.setParent(domain);
        hub.setAggrConnections(Connection.SDN, new ArrayList<AggrConnection>());
        hub.setAggrConnections(Connection.IP, new ArrayList<AggrConnection>());
        hub.setAggrConnections(Connection.PEER, new ArrayList<AggrConnection>());
        ArrayList<Node> hubNodes = new ArrayList<Node>();
        hubNodes.add(node);
        hub.setNodes(hubNodes);
        node.setParent(hub);
        return hub;
    }
}
